package com.itunic.cloud.configuration;

import com.itunic.cloud.domain.CustomOAuth2User;
import com.nimbusds.oauth2.sdk.ErrorObject;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
import net.minidev.json.JSONObject;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.AuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 获取user-info信息
 * 并构造出登录用户信息
 *
 * @author Sean
 */
public class CustomReactiveOAuth2UserService implements ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
    private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";

    private WebClient webClient;

    public CustomReactiveOAuth2UserService(WebClient webClient) {
        this.webClient = webClient;
    }

    /**
     * 加载用户，也就是发起请求，构造用户的逻辑
     *
     * @param userRequest
     * @return
     * @throws OAuth2AuthenticationException
     */
    @Override
    public Mono<OAuth2User> loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        return Mono.defer(() -> {
            //做一些请求前的参数校验
            Assert.notNull(userRequest, "userRequest cannot be null");
            String userInfoUri = userRequest.getClientRegistration().getProviderDetails()
                    .getUserInfoEndpoint().getUri();
            if (!StringUtils.hasText(
                    userInfoUri)) {
                OAuth2Error oauth2Error = new OAuth2Error(
                        MISSING_USER_INFO_URI_ERROR_CODE,
                        "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
                                + userRequest.getClientRegistration().getRegistrationId(),
                        null);
                throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
            }

            ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<Map<String, Object>>() {
            };
            AuthenticationMethod authenticationMethod = userRequest.getClientRegistration().getProviderDetails()
                    .getUserInfoEndpoint().getAuthenticationMethod();

            WebClient.RequestHeadersSpec<?> requestHeadersSpec;
            //构造请求信息
            if (AuthenticationMethod.FORM.equals(authenticationMethod)) {
                requestHeadersSpec = this.webClient.post()
                        .uri(userInfoUri)
                        .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                        .bodyValue("access_token=" + userRequest.getAccessToken().getTokenValue());
            } else {
                requestHeadersSpec = this.webClient.get()
                        .uri(userInfoUri)
                        .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                        .headers(headers -> headers.setBearerAuth(userRequest.getAccessToken().getTokenValue()));
            }
            //拿到请求结果
            Mono<Map<String, Object>> userAttributes = requestHeadersSpec
                    .retrieve()
                    .onStatus(s -> s != HttpStatus.OK, response -> parse(response).map(userInfoErrorResponse -> {
                        String description = userInfoErrorResponse.getErrorObject().getDescription();
                        OAuth2Error oauth2Error = new OAuth2Error(
                                INVALID_USER_INFO_RESPONSE_ERROR_CODE, description,
                                null);
                        throw new OAuth2AuthenticationException(oauth2Error,
                                oauth2Error.toString());
                    }))
                    .bodyToMono(typeReference);
            //根据请求结果构造用户信息
            return userAttributes.map(attrs -> makeUserInfo(attrs))
                    .onErrorMap(e -> e instanceof IOException, t ->
                            new AuthenticationServiceException("Unable to access the userInfoEndpoint " + userInfoUri, t))
                    .onErrorMap(t -> !(t instanceof AuthenticationServiceException), t -> {
                        OAuth2Error oauth2Error =
                                new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                                        "An error occurred reading the UserInfo Success response: " + t.getMessage(), null);
                        return new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), t);
                    });
        });

    }

    /**
     * 根据响应结果，构造用户信息
     * @param body
     * @return
     */
    protected CustomOAuth2User makeUserInfo(Map<String, Object> body) {
        //实例化一个自定义的用户实体
        CustomOAuth2User user = new CustomOAuth2User();
        //拿到user info 中的principal节点。
        //具体节点中有什么，可以直接拿着token 请求授权中心 user-info端点。
        Map<String, Object> principal = (Map) body.get("principal");
        //构造用户信息
        user.setUsername(String.valueOf(principal.get("username")));
        user.setId(String.valueOf(principal.get("id")));
        List authorities = (List) principal.get("authorities");
        //构造权限信息，将user-info中的权限，构造到本地
        List<SimpleGrantedAuthority> list = new ArrayList<>(authorities.size());
        authorities.forEach(f -> {
            Map f1 = (Map) f;
            list.add(new SimpleGrantedAuthority(String.valueOf(f1.get("authority"))));
        });
        user.setAuthorities(list);
        user.setAttributes(body);
        return user;
    }

    /**
     * 将响应数据序列化为json 数据，方便后续操作
     * @param httpResponse
     * @return
     */
    private static Mono<UserInfoErrorResponse> parse(ClientResponse httpResponse) {

        String wwwAuth = httpResponse.headers().asHttpHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE);
        if (!StringUtils.isEmpty(wwwAuth)) {
            return Mono.fromCallable(() -> UserInfoErrorResponse.parse(wwwAuth));
        }
        ParameterizedTypeReference<Map<String, String>> typeReference =
                new ParameterizedTypeReference<Map<String, String>>() {
                };
        return httpResponse
                .bodyToMono(typeReference)
                .map(body -> new UserInfoErrorResponse(ErrorObject.parse(new JSONObject(body))));
    }
}
